/*
 * Copyright (c) 1988 Regents of the University of California.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *      This product includes software developed by the University of
 *      California, Berkeley and its contributors.
 * 4. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

/*
 * Primitives for displaying the file on the screen.
 */

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <less.h>

#ifdef REGEX
#include <regex.h>
#endif

int back_scroll = -1;
int hit_eof;            /* keeps track of how many times we hit end of file */
int screen_trashed;

static int squished;

extern int sigs;
extern int top_scroll;
extern int sc_width, sc_height;
extern int caseless;
extern int linenums;
extern int tagoption;
extern char *line;

off_t position(), forw_line(), back_line(), forw_raw_line(), back_raw_line();
off_t ch_length(), ch_tell();

/*
 * Check to see if the end of file is currently "displayed".
 */
eof_check()
{
        off_t pos;

        if (sigs)
                return;
        /*
         * If the bottom line is empty, we are at EOF.
         * If the bottom line ends at the file length,
         * we must be just at EOF.
         */
        pos = position(BOTTOM_PLUS_ONE);
        if (pos == NULL_POSITION || pos == ch_length())
                hit_eof++;
}

/*
 * If the screen is "squished", repaint it.
 * "Squished" means the first displayed line is not at the top
 * of the screen; this can happen when we display a short file
 * for the first time.
 */
squish_check()
{
        if (squished) {
                squished = 0;
                repaint();
        }
}

/*
 * Display n lines, scrolling forward, starting at position pos in the
 * input file.  "only_last" means display only the last screenful if
 * n > screen size.
 */
forw(n, pos, only_last)
        register int n;
        off_t pos;
        int only_last;
{
        extern int short_file;
        static int first_time = 1;
        int eof = 0, do_repaint;

        squish_check();

        /*
         * do_repaint tells us not to display anything till the end, 
         * then just repaint the entire screen.
         */
        do_repaint = (only_last && n > sc_height-1);

        if (!do_repaint) {
#if 0
                if (top_scroll && n >= sc_height - 1) {
                        /*
                         * Start a new screen.
                         * {{ This is not really desirable if we happen
                         *    to hit eof in the middle of this screen,
                         *    but we don't yet know if that will happen. }}
                         */
                        clear();
                        home();
                } else {
                        lower_left();
                        clear_eol();
                }
#endif

                /*
                 * This is not contiguous with what is currently displayed.
                 * Clear the screen image (position table) and start a new
                 * screen.
                 */
                if (pos != position(BOTTOM_PLUS_ONE)) {
                        pos_clear();
                        add_forw_pos(pos);
                        if (top_scroll) {
                                clear();
                                home();
                        } else if (!first_time)
                                putstr("...skipping...\n");
                }
        }

        for (short_file = 0; --n >= 0;) {
                /*
                 * Read the next line of input.
                 */
                pos = forw_line(pos);
                if (pos == NULL_POSITION) {
                        /*
                         * end of file; copy the table if the file was
                         * too small for an entire screen.
                         */
                        eof = 1;
                        if (position(TOP) == NULL_POSITION) {
                                copytable();
                                if (!position(TOP))
                                        short_file = 1;
                        }
                        break;
                }
                /*
                 * Add the position of the next line to the position table.
                 * Display the current line on the screen.
                 */
                add_forw_pos(pos);
                if (do_repaint)
                        continue;
                /*
                 * If this is the first screen displayed and we hit an early
                 * EOF (i.e. before the requested number of lines), we
                 * "squish" the display down at the bottom of the screen.
                 * But don't do this if a -t option was given; it can cause
                 * us to start the display after the beginning of the file,
                 * and it is not appropriate to squish in that case.
                 */
                if (first_time && line == NULL && !top_scroll && !tagoption) {
                        squished = 1;
                        continue;
                }
                put_line();
        }

        if (eof && !sigs)
                hit_eof++;
        else
                eof_check();
        if (do_repaint)
                repaint();
        first_time = 0;
        (void) currline(BOTTOM);
}

/*
 * Display n lines, scrolling backward.
 */
back(n, pos, only_last)
        register int n;
        off_t pos;
        int only_last;
{
        int do_repaint;

        squish_check();
        do_repaint = (n > get_back_scroll() || (only_last && n > sc_height-1));
        hit_eof = 0;
        while (--n >= 0)
        {
                /*
                 * Get the previous line of input.
                 */
                pos = back_line(pos);
                if (pos == NULL_POSITION)
                        break;
                /*
                 * Add the position of the previous line to the position table.
                 * Display the line on the screen.
                 */
                add_back_pos(pos);
                if (!do_repaint)
                {
                        home();
                        add_line();
                        put_line();
                }
        }

        eof_check();
        if (do_repaint)
                repaint();
        (void) currline(BOTTOM);
}

/*
 * Display n more lines, forward.
 * Start just after the line currently displayed at the bottom of the screen.
 */
forward(n, only_last)
        int n;
        int only_last;
{
        off_t pos;

        if (hit_eof) {
                /*
                 * If we're trying to go forward from end-of-file,
                 * go on to the next file.
                 */
                next_file(1);
                return;
        }

        pos = position(BOTTOM_PLUS_ONE);
        if (pos == NULL_POSITION)
        {
                hit_eof++;
                return;
        }
        forw(n, pos, only_last);
}

/*
 * Display n more lines, backward.
 * Start just before the line currently displayed at the top of the screen.
 */
backward(n, only_last)
        int n;
        int only_last;
{
        off_t pos;

        pos = position(TOP);
        /*
         * This will almost never happen, because the top line is almost
         * never empty.
         */
        if (pos == NULL_POSITION)
                return;   
        back(n, pos, only_last);
}

/*
 * Repaint the screen, starting from a specified position.
 */
prepaint(pos)
        off_t pos;
{
        hit_eof = 0;
        forw(sc_height-1, pos, 0);
        screen_trashed = 0;
}

/*
 * Repaint the screen.
 */
repaint()
{
        /*
         * Start at the line currently at the top of the screen
         * and redisplay the screen.
         */
        prepaint(position(TOP));
}

/*
 * Jump to the end of the file.
 * It is more convenient to paint the screen backward,
 * from the end of the file toward the beginning.
 */
jump_forw()
{
        off_t pos;

        if (ch_end_seek())
        {
                error("Cannot seek to end of file");
                return;
        }
        lastmark();
        pos = ch_tell();
        clear();
        pos_clear();
        add_back_pos(pos);
        back(sc_height - 1, pos, 0);
}

/*
 * Jump to line n in the file.
 */
jump_back(n)
        register int n;
{
        register int c, nlines;

        /*
         * This is done the slow way, by starting at the beginning
         * of the file and counting newlines.
         *
         * {{ Now that we have line numbering (in linenum.c),
         *    we could improve on this by starting at the
         *    nearest known line rather than at the beginning. }}
         */
        if (ch_seek((off_t)0)) {
                /* 
                 * Probably a pipe with beginning of file no longer buffered. 
                 * If he wants to go to line 1, we do the best we can, 
                 * by going to the first line which is still buffered.
                 */
                if (n <= 1 && ch_beg_seek() == 0)
                        jump_loc(ch_tell());
                error("Cannot get to beginning of file");
                return;
        }

        /*
         * Start counting lines.
         */
        for (nlines = 1;  nlines < n;  nlines++)
                while ((c = ch_forw_get()) != '\n')
                        if (c == EOI) {
                                char message[40];
                                (void)sprintf(message, "File has only %d lines",
                                    nlines - 1);
                                error(message);
                                return;
                        }
        jump_loc(ch_tell());
}

/*
 * Jump to a specified percentage into the file.
 * This is a poor compensation for not being able to
 * quickly jump to a specific line number.
 */
jump_percent(percent)
        int percent;
{
        off_t pos, len, ch_length();
        register int c;

        /*
         * Determine the position in the file
         * (the specified percentage of the file's length).
         */
        if ((len = ch_length()) == NULL_POSITION)
        {
                error("Don't know length of file");
                return;
        }
        pos = (percent * len) / 100;

        /*
         * Back up to the beginning of the line.
         */
        if (ch_seek(pos) == 0)
        {
                while ((c = ch_back_get()) != '\n' && c != EOI)
                        ;
                if (c == '\n')
                        (void) ch_forw_get();
                pos = ch_tell();
        }
        jump_loc(pos);
}

/*
 * Jump to a specified position in the file.
 */
jump_loc(pos)
        off_t pos;
{
        register int nline;
        off_t tpos;

        if ((nline = onscreen(pos)) >= 0) {
                /*
                 * The line is currently displayed.  
                 * Just scroll there.
                 */
                forw(nline, position(BOTTOM_PLUS_ONE), 0);
                return;
        }

        /*
         * Line is not on screen.
         * Seek to the desired location.
         */
        if (ch_seek(pos)) {
                error("Cannot seek to that position");
                return;
        }

        /*
         * See if the desired line is BEFORE the currently displayed screen.
         * If so, then move forward far enough so the line we're on will be
         * at the bottom of the screen, in order to be able to call back()
         * to make the screen scroll backwards & put the line at the top of
         * the screen.
         * {{ This seems inefficient, but it's not so bad,
         *    since we can never move forward more than a
         *    screenful before we stop to redraw the screen. }}
         */
        tpos = position(TOP);
        if (tpos != NULL_POSITION && pos < tpos) {
                off_t npos = pos;
                /*
                 * Note that we can't forw_line() past tpos here,
                 * so there should be no EOI at this stage.
                 */
                for (nline = 0;  npos < tpos && nline < sc_height - 1;  nline++)
                        npos = forw_line(npos);

                if (npos < tpos) {
                        /*
                         * More than a screenful back.
                         */
                        lastmark();
                        clear();
                        pos_clear();
                        add_back_pos(npos);
                }

                /*
                 * Note that back() will repaint() if nline > back_scroll.
                 */
                back(nline, npos, 0);
                return;
        }
        /*
         * Remember where we were; clear and paint the screen.
         */
        lastmark();
        prepaint(pos);
}

/*
 * The table of marks.
 * A mark is simply a position in the file.
 */
#define NMARKS          (27)            /* 26 for a-z plus one for quote */
#define LASTMARK        (NMARKS-1)      /* For quote */
static off_t marks[NMARKS];

/*
 * Initialize the mark table to show no marks are set.
 */
init_mark()
{
        int i;

        for (i = 0;  i < NMARKS;  i++)
                marks[i] = NULL_POSITION;
}

/*
 * See if a mark letter is valid (between a and z).
 */
        static int
badmark(c)
        int c;
{
        if (c < 'a' || c > 'z')
        {
                error("Choose a letter between 'a' and 'z'");
                return (1);
        }
        return (0);
}

/*
 * Set a mark.
 */
setmark(c)
        int c;
{
        if (badmark(c))
                return;
        marks[c-'a'] = position(TOP);
}

lastmark()
{
        marks[LASTMARK] = position(TOP);
}

/*
 * Go to a previously set mark.
 */
gomark(c)
        int c;
{
        off_t pos;

        if (c == '\'') {
                pos = marks[LASTMARK];
                if (pos == NULL_POSITION)
                        pos = 0;
        }
        else {
                if (badmark(c))
                        return;
                pos = marks[c-'a'];
                if (pos == NULL_POSITION) {
                        error("mark not set");
                        return;
                }
        }
        jump_loc(pos);
}

/*
 * Get the backwards scroll limit.
 * Must call this function instead of just using the value of
 * back_scroll, because the default case depends on sc_height and
 * top_scroll, as well as back_scroll.
 */
get_back_scroll()
{
        if (back_scroll >= 0)
                return (back_scroll);
        if (top_scroll)
                return (sc_height - 2);
        return (sc_height - 1);
}

/*
 * Search for the n-th occurence of a specified pattern, 
 * either forward or backward.
 */
search(search_forward, pattern, n, wantmatch)
        register int search_forward;
        register char *pattern;
        register int n;
        int wantmatch;
{
        off_t pos, linepos;
        register char *p;
        register char *q;
        int linenum;
        int linematch;
#ifdef REGEX
        static regex_t *cpattern = NULL;
#else
#ifdef RECOMP
        char *re_comp();
        char *errmsg;
#else
#ifdef REGCMP
        char *regcmp();
        static char *cpattern = NULL;
#else
        static char lpbuf[100];
        static char *last_pattern = NULL;
        char *strcpy();
#endif
#endif
#endif /*REGEX */
        /*
         * For a caseless search, convert any uppercase in the pattern to
         * lowercase.
         */
        if (caseless && pattern != NULL)
                for (p = pattern;  *p;  p++)
                        if (isupper(*p))
                                *p = tolower(*p);
#ifdef REGEX
        if (pattern == NULL || *pattern == '\0')
        {
                /*
                 * A null pattern means use the previous pattern.
                 * The compiled previous pattern is in cpattern, so just use it.
                 */
                if (cpattern == NULL)
                {
                        error("No previous regular expression");
                        return(0);
                }
        } else
        {
                /*
                 * Otherwise compile the given pattern.
                 */
                if (cpattern == NULL
                 && (cpattern = (regex_t *) malloc(sizeof(regex_t))) == NULL) {
                        error("cannot allocate memory");
                        quit();
                }
                else
                        regfree(cpattern);
                if (regcomp(cpattern, pattern, 0))
                {
                        error("Invalid pattern");
                        return(0);
                }
        }
#else
#ifdef RECOMP

        /*
         * (re_comp handles a null pattern internally, 
         *  so there is no need to check for a null pattern here.)
         */
        if ((errmsg = re_comp(pattern)) != NULL)
        {
                error(errmsg);
                return(0);
        }
#else
#ifdef REGCMP
        if (pattern == NULL || *pattern == '\0')
        {
                /*
                 * A null pattern means use the previous pattern.
                 * The compiled previous pattern is in cpattern, so just use it.
                 */
                if (cpattern == NULL)
                {
                        error("No previous regular expression");
                        return(0);
                }
        } else
        {
                /*
                 * Otherwise compile the given pattern.
                 */
                char *s;
                if ((s = regcmp(pattern, 0)) == NULL)
                {
                        error("Invalid pattern");
                        return(0);
                }
                if (cpattern != NULL)
                        free(cpattern);
                cpattern = s;
        }
#else
        if (pattern == NULL || *pattern == '\0')
        {
                /*
                 * Null pattern means use the previous pattern.
                 */
                if (last_pattern == NULL)
                {
                        error("No previous regular expression");
                        return(0);
                }
                pattern = last_pattern;
        } else
        {
                (void)strcpy(lpbuf, pattern);
                last_pattern = lpbuf;
        }
#endif
#endif
#endif /* REGEX */

        /*
         * Figure out where to start the search.
         */

        if (position(TOP) == NULL_POSITION) {
                /*
                 * Nothing is currently displayed.  Start at the beginning
                 * of the file.  (This case is mainly for searches from the
                 * command line.
                 */
                pos = (off_t)0;
        } else if (!search_forward) {
                /*
                 * Backward search: start just before the top line
                 * displayed on the screen.
                 */
                pos = position(TOP);
        } else {
                /*
                 * Start at the second screen line displayed on the screen.
                 */
                pos = position(TOP_PLUS_ONE);
        }

        if (pos == NULL_POSITION)
        {
                /*
                 * Can't find anyplace to start searching from.
                 */
                error("Nothing to search");
                return(0);
        }

        linenum = find_linenum(pos);
        for (;;)
        {
                /*
                 * Get lines until we find a matching one or 
                 * until we hit end-of-file (or beginning-of-file 
                 * if we're going backwards).
                 */
                if (sigs)
                        /*
                         * A signal aborts the search.
                         */
                        return(0);

                if (search_forward)
                {
                        /*
                         * Read the next line, and save the 
                         * starting position of that line in linepos.
                         */
                        linepos = pos;
                        pos = forw_raw_line(pos);
                        if (linenum != 0)
                                linenum++;
                } else
                {
                        /*
                         * Read the previous line and save the
                         * starting position of that line in linepos.
                         */
                        pos = back_raw_line(pos);
                        linepos = pos;
                        if (linenum != 0)
                                linenum--;
                }

                if (pos == NULL_POSITION)
                {
                        /*
                         * We hit EOF/BOF without a match.
                         */
                        error("Pattern not found");
                        return(0);
                }

                /*
                 * If we're using line numbers, we might as well
                 * remember the information we have now (the position
                 * and line number of the current line).
                 */
                if (linenums)
                        add_lnum(linenum, pos);

                /*
                 * If this is a caseless search, convert uppercase in the
                 * input line to lowercase.
                 */
                if (caseless)
                        for (p = q = line;  *p;  p++, q++)
                                *q = isupper(*p) ? tolower(*p) : *p;

                /*
                 * Remove any backspaces along with the preceeding char.
                 * This allows us to match text which is underlined or
                 * overstruck.
                 */
                for (p = q = line;  *p;  p++, q++)
                        if (q > line && *p == '\b')
                                /* Delete BS and preceeding char. */
                                q -= 2;
                        else
                                /* Otherwise, just copy. */
                                *q = *p;

                /*
                 * Test the next line to see if we have a match.
                 * This is done in a variety of ways, depending
                 * on what pattern matching functions are available.
                 */
#ifdef REGEX
                linematch = !regexec(cpattern, line, 0, NULL, 0);
#else
#ifdef REGCMP
                linematch = (regex(cpattern, line) != NULL);
#else
#ifdef RECOMP
                linematch = (re_exec(line) == 1);
#else
                linematch = match(pattern, line);
#endif
#endif
#endif /* REGEX */
                /*
                 * We are successful if wantmatch and linematch are
                 * both true (want a match and got it),
                 * or both false (want a non-match and got it).
                 */
                if (((wantmatch && linematch) || (!wantmatch && !linematch)) &&
                      --n <= 0)
                        /*
                         * Found the line.
                         */
                        break;
        }
        jump_loc(linepos);
        return(1);
}

#if !defined(REGCMP) && !defined(RECOMP) && !defined(REGEX)
/*
 * We have neither regcmp() nor re_comp().
 * We use this function to do simple pattern matching.
 * It supports no metacharacters like *, etc.
 */
static
match(pattern, buf)
        char *pattern, *buf;
{
        register char *pp, *lp;

        for ( ;  *buf != '\0';  buf++)
        {
                for (pp = pattern, lp = buf;  *pp == *lp;  pp++, lp++)
                        if (*pp == '\0' || *lp == '\0')
                                break;
                if (*pp == '\0')
                        return (1);
        }
        return (0);
}
#endif
